using System;
using System.Reflection;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Routing;
using System.Runtime.Serialization;
using Kollektiv.Config;
using Kollektiv.Extension;
using System.Web.Mvc;
using Kollektiv.Validation;
using System.Collections.Specialized;

namespace Kollektiv.Web.Mvc
{
	#region config objects
    [DataContract]
    public class RouteServiceConfig
    {
        [DataMember(Order = 0)]
        public string DefaultRoute { get; set; }

        [DataMember(Order = 1)]
        public IEnumerable<RouteServiceInstanceConfig> Instances { get; set; }

        [DataMember(Order = 2)]
        public IEnumerable<RouteOverride> Overrides { get; set; }
    }

    [DataContract]
    public class RouteServiceInstanceConfig
    {
        [DataMember(Order = 0)]
        public string ID { get; set; }

        [DataMember(Order = 1)]
        public string ControllerAssemblyName { get; set; }

        public Assembly ControllerAssembly
        {
            get { return this.ControllerAssemblyName.FindAssembly(); }
            set { this.ControllerAssemblyName = value.GetName().Name; }
        }
    }

    [DataContract]
    public class RouteOverride
    {
        [DataMember(Order = 0)]
        public string From { get; set; }

        [DataMember(Order = 1)]
        public string To { get; set; }
    }

	#endregion
	
    /// <summary>
    /// This service manages and processes all routes, controller and actions
    /// It is an extension to ASP.NET MVC for supporting multiple controller assemblies.
    /// </summary>
	public class RouteService
	{
		public const string _ROUTE_STRING = "route";
		public const string _CONTROLLER_STRING = "controller";
		public const string _ACTION_STRING = "action";
		private static RouteServiceConfig config;
		private static Dictionary<string, RouteService> instances;

        public static RouteService MainInstance { get; private set; }
		
		static RouteService ()
		{
			config = ConfigService.Load<RouteServiceConfig> ();
			instances = new Dictionary<string, RouteService> ();

            MainInstance = new RouteService(new RouteServiceInstanceConfig());
		}
		
        /// <summary>
        /// Gets or creates route services for all configured routes.
        /// </summary>
        /// <returns>Collection of all route services.</returns>
		public static IEnumerable<RouteService> GetInstances ()
		{
			List<RouteService> routeServices = new List<RouteService> ();
			
			foreach (RouteServiceInstanceConfig instanceConfig in config.Instances) {
				routeServices.Add (GetInstance (instanceConfig.ID));
			}
			
			return routeServices.AsReadOnly ();
		}
		
        /// <summary>
        /// Gets or creates a route service for a configured route.
        /// </summary>
        /// <param name="id">Id of the configured route.</param>
        /// <returns>The route service.</returns>
		public static RouteService GetInstance (string id)
		{
			if (config.Instances.Where (ic => ic.ID == id).Any ()) {
				if (!instances.ContainsKey (id))
					instances.Add (id, new RouteService (config.Instances.Where (ic => ic.ID == id).First ()));
				
				return instances [id];
			}
			
			return null;
		}

        internal static string GetOverride(string from)
        {
            RouteOverride routeOverride = config.Overrides.Where(o => o.From == from).FirstOrDefault();
            return routeOverride != null ? routeOverride.To : null;
        }
		
		private	RouteServiceInstanceConfig instanceConfig;
		private IEnumerable<MethodInfo > actions = null;
		
        /// <summary>
        /// Gets all actions found in the controller assembly.
        /// </summary>
		public IEnumerable<MethodInfo > Actions {
			get {
                if (this.actions == null && this.instanceConfig.ControllerAssembly != null)
					this.actions = this.instanceConfig.ControllerAssembly.FindControllerActions ();
				
				return this.actions;
			}
		}
		
        /// <summary>
        /// Gets the id of the route of this route service.
        /// </summary>
		public string ID { get { return this.instanceConfig.ID; } }
		
        /// <summary>
        /// The route needed to add to the MVC route collection.
        /// </summary>
        /// <example>
        /// This example shows howto load and register in Global.asax.cs all configured routes and overrides.
        /// <code>
        /// public static void RegisterRoutes (RouteCollection routes)
        /// {
        ///     routes.Add(RouteService.MainInstance.ID, RouteService.MainInstance.Route);
        ///     foreach (RouteService routeService in RouteService.GetInstances())
        ///     {
        ///         routes.Add (routeService.ID, routeService.Route);
        ///     }
        /// }
        /// </code>
        /// </example>
		public Route Route { get; private set; }

        private ControllerFactory controllerFactory;
		
		public RouteService (RouteServiceInstanceConfig instanceConfig)
		{
			this.instanceConfig = instanceConfig;
            this.CheckConfig();

            RouteValueDictionary defaults = null;
            if (!string.IsNullOrEmpty(this.instanceConfig.ID))
            {
                this.Route = new Route(string.Concat(this.ID, "/{", _CONTROLLER_STRING, "}/{", _ACTION_STRING, "}"), defaults, new RouteHandler(this.instanceConfig.ID));
            }
            else
            {
                defaults = new RouteValueDictionary();
                defaults.Add(_ROUTE_STRING, string.Empty);

                this.Route = new Route(string.Concat("{", _ROUTE_STRING, "}"), defaults, new RouteHandler(null));
            }

            this.controllerFactory = new ControllerFactory(this.instanceConfig.ID);
		}

        private void CheckConfig()
        {
            if (!string.IsNullOrEmpty(this.instanceConfig.ID))
            {
                if (this.instanceConfig.ControllerAssembly == null)
                {
                    throw (new ConfigException(
                        string.Concat("controller assembly has to be set in config of route <", this.instanceConfig.ID, ">.")));
                }
            }
        }
		
		internal void ProcessRequest (RequestContext requestContext, HttpContext httpContext)
		{
            string controller = requestContext.RouteData.GetRequiredString(_CONTROLLER_STRING);
            string action = requestContext.RouteData.GetRequiredString(_ACTION_STRING);
			
			IEnumerable<MethodInfo > actionsQuery = this.Actions
				.Where (a => a.DeclaringType.Name == controller && a.Name == action);
			
			if (!actionsQuery.Any ())
				throw(new InvalidOperationException (string.Concat ("there is no action <", action, "> in controller <", controller, ">")));
			
			MethodInfo actionMethod = actionsQuery.First ();

            ControllerBuilder.Current.SetControllerFactory(this.controllerFactory);
            Controller controllerInstance = (Controller)ControllerBuilder.Current.GetControllerFactory().CreateController(requestContext, actionMethod.DeclaringType.FullName);
			object[] parameters = this.ProcessParameters (actionMethod, requestContext);
			this.ProcessResult(this.InvokeAction (actionMethod, controllerInstance, parameters), controllerInstance.ControllerContext);
		}

        private void ProcessResult(object returnValue, ControllerContext context)
        {
            ActionResult result;
            if (returnValue is ActionResult)
            {
                result = (ActionResult)returnValue;
            }
            else
            {
                result = new JsonResult { Data = returnValue, JsonRequestBehavior = JsonRequestBehavior.AllowGet };
            }

            result.ExecuteResult(context);
        }
		
		private object[] ProcessParameters (MethodInfo actionMethod, RequestContext requestContext)
		{
			List<object > parameters = new List<object> ();
			string controller = requestContext.RouteData.GetRequiredString (_CONTROLLER_STRING);
			string action = requestContext.RouteData.GetRequiredString (_ACTION_STRING);
			NameValueCollection requestParameters = requestContext.HttpContext.Request.Params;
			
			foreach (ParameterInfo actionParameter in actionMethod.GetParameters()) {
				object parameterValue = null;
				
				if (requestParameters.AllKeys.Contains (actionParameter.Name)) {
					string rawParameterValue = requestParameters [actionParameter.Name];
					parameterValue = Convert.ChangeType (rawParameterValue, actionParameter.ParameterType);
				}
				
				if (actionParameter.ParameterType.IsValueType && parameterValue == null)
					throw(new InvalidOperationException (
						string.Concat ("controller <", controller, "> action<", action, "> need's a parameter <", actionParameter.Name, "> of type <", actionParameter.ParameterType.ToString (), ">")));
				
				IEnumerable<ParameterValidatorAttribute > validators = ParameterValidatorAttribute.GetValidatiorAttributes (actionParameter);
				
				foreach (ParameterValidatorAttribute validator in validators)
					validator.Validate (parameterValue, actionParameter.Name);
				
				parameters.Add (parameterValue);
			}
			
			return parameters.ToArray ();
		}

        private object InvokeAction(MethodInfo actionMethod, IController controller, object[] parameters)
        {
            object retVal = null;
            object retValPre = this.ProcessPreActionHandler(actionMethod, controller);
            if (retValPre == null)
            {
                retVal = actionMethod.Invoke(controller, parameters);
                object retValPost = this.ProcessPostActionHandler(actionMethod, controller);

                retVal = retValPost ?? retVal;
            }
            else
            {
                retVal = retValPre;
            }

            return retVal;
        }
		
		private object ProcessPreActionHandler (MethodInfo actionMethod, IController controller)
		{
            object retVal = null;
			foreach (object controllerAttribute in controller.GetType ().GetCustomAttributes (true)) {
				if (controllerAttribute is ControllerAttribute && !string.IsNullOrEmpty (((ControllerAttribute)controllerAttribute).PreActionHandler)) {
					retVal = controller.GetType ().GetMethodExtended (((ControllerAttribute)controllerAttribute).PreActionHandler)
						.Invoke (controller, null);

                    if (retVal != null) break;
				}
			}

            if (retVal == null)
            {
                foreach (object controllerActionAttribute in actionMethod.GetCustomAttributes(true))
                {
                    if (controllerActionAttribute is ControllerActionAttribute && !string.IsNullOrEmpty(((ControllerActionAttribute)controllerActionAttribute).PreActionHandler))
                    {
                        retVal = controller.GetType().GetMethodExtended(((ControllerActionAttribute)controllerActionAttribute).PreActionHandler)
                            .Invoke(controller, null);

                        if (retVal != null) break;
                    }
                }
            }

            return retVal;
		}
		
		private object ProcessPostActionHandler (MethodInfo actionMethod, IController controller)
        {
            object retVal = null;
			foreach (object controllerAttribute in controller.GetType ().GetCustomAttributes (true)) {
				if (controllerAttribute is ControllerAttribute && !string.IsNullOrEmpty (((ControllerAttribute)controllerAttribute).PostActionHandler)) {
                    retVal = controller.GetType().GetMethodExtended(((ControllerAttribute)controllerAttribute).PostActionHandler)
                        .Invoke(controller, null);

                    if (retVal != null) break;
				}
			}

            if (retVal == null)
            {
                foreach (object controllerActionAttribute in actionMethod.GetCustomAttributes(true))
                {
                    if (controllerActionAttribute is ControllerActionAttribute && !string.IsNullOrEmpty(((ControllerActionAttribute)controllerActionAttribute).PostActionHandler))
                    {
                        retVal = controller.GetType().GetMethodExtended(((ControllerActionAttribute)controllerActionAttribute).PostActionHandler)
                            .Invoke(controller, null);

                        if (retVal != null) break;
                    }
                }
            }

            return retVal;
		}
	}
}